iT邦幫忙

2023 iThome 鐵人賽

DAY 26
1
Modern Web

了不起的 Svelte系列 第 26 篇

第 26 天:Svelte 的互動百寶箱:Store(三)

  • 分享至 

  • xImage
  •  

第 26 天:Svelte 的互動百寶箱:Store(三)

「恩,就是這麼一回事了。我漸漸離我的抱負越來越遠,漸漸在她的愛中沉的越來越深,然後突然之間我什麼都不在意了。在應用程式當中觸發各種事件又有什麼用呢?還不如好好地跳一些通知給她,告訴她我又觸發了哪些事件。」

~節錄自《The Great Svelte:第八章》

第 26 天要講的事

  1. 動手練習 Store
  2. 從 App.svelte 取得 toastStore 的資料
  3. 從 Palettes.svelte 取得 toastStore 的資料

  經過兩天的介紹,對於 Svelte 提供的 Store 物件應該有初步的概念了。今天就讓我們試著在色票產生器這個專案當中來使用 Store,一邊練習一邊加深我們對 Store 的理解吧!

動手練習 Store

  一開始在介紹 Store 的時候,我們說 Store 可以實作出水平傳遞的資料,讓沒有互相隸屬的兩個 Svelte 元件仍然可以透過 Store 來溝通。什麼情況會需要用到這種設計呢?在我們現在色票產生器這個專案裡頭,如果想要實作出一個訊息中心,每當使用者在互動介面上做出新的操作,就跳出訊息來提示使用者。這樣子的一個訊息中心,需要從各個不同的 Svelte 元件獲取事件發生的詳細情況,也就是需要與各種不同的 Svelte 元件進行資料溝通。所以如果想要實作出訊息中心,看起來就適合使用 Store 所提供的水平傳遞資料的特性,做出一個 Store 物件來儲存所有的訊息,並且讓各個不同的 Svelte 都能將各自發生的事件直接傳達到這個 Store 物件當中。
  在現代的網頁設計當中,一則一則隨著事件發生而跳出來提示使用者的訊息有個可愛的暱稱,叫做 Toast。所以我們就來做個 Toast Store 吧。首先在 /src/lib 當中開啟一個新的 Javascript 檔案 toastStore:

/src/lib/toastStore.js
import { writable } from "svelte/store";

const createToastStore = () => {
  let uuid = 0;
  const { subscribe, update } = writable([]);

  return {
    subscribe,
    addToast: ({ action, hex }) => {
      const id = uuid++;
      update(state => [...state, { action, hex, id: id }])
      setTimeout(() => update(state => state.filter(x => x.id !== id)), 5000)
    },
    removeToast: ({ id }) => update(state => state.filter(x => x.id !== id)),
  }
}

export const toastStore = createToastStore();
  • 第一行:import { writable } from "svelte/store";
      先引入 writable 這個 Svelte 所提供最基礎萬用的 Store。

  • 第三行:const createToastStore = () => {
      宣告一個函式,createToastStore,讓我們從 writable 這個最基礎的 Store 出發,並包裝成一個適合當作訊息中心的 toast Store。

  • 第四行:let uuid = 0;
      宣告一個變數 uuid,這個變數將會替每一則新加入的訊息標上識別碼。

  • 第五行:const { subscribe, update } = writable([]);
      初始化一個 writable Store。並從中取出我們需要的方法 subscribe 跟 update。

  • 第七行:return {
      回傳一個物件。這個物件將會提供客製化的方式來更新我們的 writable Store。

  • 第八行:subscribe,
      首先原封不動的回傳 subscribe 這個方法。

  • 第九行:addToast: ({ action, hex }) => {
      並且實作出一個新方法 addToast。這個新的方法會需要一個具有 action 跟 hex 兩種資料的物件當作參數,執行的時候會將識別碼 uuid 增加一,藉此達到獨一無二的效果。

  • 第十一行:update(state => [...state, { action, hex, id: id }])
      接著利用 update 將我們的 writable 物件內的資料做更新。更新的方法是加入一個具有 action、hex、以及 id 這三種資料的物件。

  • 第十二行:setTimeout(() => update(state => state.filter(x => x.id !== id)), 5000)
      我們不希望訊息中心跳出來的提示一直顯示在畫面當中,所以經過 5000 毫秒之後,利用 setTimeout 將這個訊息又從 writable 的物件中刪除。

  • 第十四行:removeToast: ({ id }) => update(state => state.filter(x => x.id !== id)),
      實作一個新方法 removeToast。這個方法會需要一個具有 id 資料的物件作為參數,並且依此刪掉 writable 訊息中心相對應的那一則訊息。

  • 第十八行:export const toastStore = createToastStore();
      執行 createToastStore。這樣一來就會得到一個具有 subscribe、addToast、removeToast 這三種方法的物件。由於這三種方法是源自於 writable Store 的 subscribe 跟 update 變化而來,所以這個新的物件也是一種 Store(其實就是一個包裝過的 writable Store)。而這個新的 Store 當中所儲存的變數會是一個陣列,陣列當中一則一則的物件,具有 action、hex、跟 id,就是我們想要呈現的訊息跟每個訊息的詳細資料。我們把這個新種類的 Store 命名為 toastStore,並且用 export 關鍵字讓其他檔案也能使用。

  沒錯就是這麼簡單,短短十八行就可以做出一間美味可口的 toastStore.js!

  並且做一個 Svelte 元件 Toast.svelte 來呈現我們美味的 toast:

/src/lib/Toast.svelte
<script>
  import { toastStore } from "./toastStore";
  import plus from "../assets/plus.svg";
  import minus from "../assets/minus.svg";
  import lock from "../assets/lock.svg";
  import unlock from "../assets/unlock.svg";
  export let toast;

  const icons = { plus, minus, lock, unlock };
  $: icon = icons[toast.action];
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
  <div class="icon">
    <img src={icon} alt={toast.action} />
  </div>
  <div class="hex" style="color: #{toast.hex}">{toast.hex}</div>
</div>

<!-- CSS的部分先略過不呈現,詳細可見文末附錄 -->
  • 第二行:import { toastStore } from "./toastStore";
      引入我們做好的 toastStore。

  • 第七行:export let toast;
      宣告一個 Toast.svelte 的 Property toast。

  • 第十四行:<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
      這就是我們一則一則 toast 在 HTML 上面呈現出來的本體。加上一個 on:click 事件處理器,讓使用者可以透過點擊 toast 本體直接點掉這個提示訊息。

從 App.svelte 取得 toastStore 的資料

  既然有了 toastStore 這個美味的 Store,也有了用來呈現 toastStore 資料的 Svelte 元件 Toast.svelte,那麼我們就來到 App.svelte 使用看看吧:

/src/App.svelte
<script>
  /* 省略與今天主題無關的內容 */
  import Toast from "./lib/Toast.svelte";
  import { toastStore } from "./lib/toastStore";

  const handleClick = (e) => {
    console.log(e);
    const tobeCount = count + e.detail;
    if (0 <= tobeCount && tobeCount < 7) {
      switch (e.detail) {
        case 1:
          palettes = [...palettes, generatePalette()];
          toastStore.addToast({
            action: "plus",
            hex: palettes.slice(-1)[0].hex,
          });
          break;
        case -1:
          toastStore.addToast({
            action: "minus",
            hex: palettes.slice(-1)[0].hex,
          });
          palettes = palettes.slice(0, -1);
          break;
      }
    } else showModal = true;
  };
</script>

<!-- 將 Toast 放在這邊 -->
{#each $toastStore as toast}
  <Toast {toast} />
{/each}

<main>
  <!-- 省略與今天主題無關的內容 -->
</main>

<Modal {showModal} on:closeModal={() => (showModal = false)} />
  • 第三行:import Toast from "./lib/Toast.svelte";
      引入需要的 Svelte 元件 Toast.svelte。

  • 第四行:import { toastStore } from "./lib/toastStore";
      從 Javascript 檔案 toastStore.js 當中引入 toastStore 這個 Store 物件。

  • 第十二行:palettes = [...palettes, generatePalette()];
      如果 <Counter /> 當中代表加一的事件發生時,替我們的 palettes 多加入一個色票。

  • 第十三行: toastStore.addToast({
      並且用 addToast 這個方法,將發生的事件 action: "plus" 跟新加入的色碼 hex: palettes.slice(-1)[0].hex 包裝起來儲存到 toastStore 這個 Store 裡面的陣列當中。也就是新增一個「色票加一」的通知。

  • 第十九行:toastStore.addToast({
      如果 <Counter /> 當中代表減一的事件發生時,先將即將發生的事件 action: "minus" 跟準備要被移除的色碼 hex: palettes.slice(-1)[0].hex 包裝起來儲存到 toastStore 這個 Store 裡面的陣列當中。也就是新增一個「色票減一」的通知。

  • 第二十三行:palettes = palettes.slice(0, -1);
      然後就將色票減一。

  • 第三十一行:{#each $toastStore as toast}
      利用第 25 天:Svelte 的互動百寶箱:Store(二)介紹過的自動訂閱 (auto-subscription) 的作法,直接用 $toastStore 取得 toastStore 當中變數的值。因為這個變數是一個陣列,所以使用 {#each} 展開 each 邏輯區塊來迭代陣列當中的各個項目,也就是各個訊息。

  • 第三十二行:<Toast {toast} />
      每一則訊息就做一個 <Toast />,並且將訊息的詳細內容用 toast Property 傳遞給 <Toast />。

  • 第三十三行:{/each}
      結束 each 邏輯區塊。

https://i.ibb.co/n0jzPCn/26.gif
圖一、一則一則跳出來的可愛 toast

  以往 Svelte 元件間要互相傳遞資料只能透過 Property,因此在設計檔案架構時,需要共用相同資料的 Svelte 元件必為從屬關係,讓資料可以從父元件 (parent component) 向子元件 (child component) 傳遞,或是透過 bind 讓子元件 (child component) 去修改父元件 (parent) 的資料。現在有了 Store,需要共用相同資料很簡單,只要在 Javascript 的段落中用 import 直接將需要的 Store 引入進來就可以。

從 Palettes.svelte 取得 toastStore 的資料

  除了在 App.svelte 可以直接獲取並更新 toastStore 當中儲存的變數之外,讓我們也試試看從 Palettes.svelte 來對 toastStore 儲存的資料做改變:

/src/lib/Palettes.svelte
<script>
  import unlock from "../assets/unlock.svg";
  import lock from "../assets/lock.svg";
  import { toastStore } from "./toastStore";
  export let palettes;

  /* 省略與今天主題無關的內容 */
  const handleLock = ({ locked, hex }) =>
    toastStore.addToast({ action: locked ? "lock" : "unlock", hex });
</script>

<div class="palettes">
  <!-- 省略與今天主題無關的內容 -->

      <div class="lock-icon">
        <label>
          <input
            type="checkbox"
            bind:checked={locked}
            on:change={() => handleLock({ locked, hex })}
          />
          {#if locked}
            <img src={lock} alt="color-locked" />
          {:else}
            <img src={unlock} alt="color-unlocked" />
          {/if}
        </label>
      </div>

  <!-- 省略與今天主題無關的內容 -->
</div>
  • 第四行:import { toastStore } from "./toastStore";
      從 toastStore.js 當中引入需要的 toastStore。

  • 第八行:const handleLock = ({ locked, hex }) =>
      宣告一個事件處理器 handleLock,用來處理上鎖/解鎖的事件。雖然我們已經用 bind 讓主要元件 App.svelte 當中的變數 palettes 可以直接做改變了,但現在除了 palettes 需要做改變之外,我們也需要改變 toastStore 當中的變數。所以這邊多實作一個事件處理器去完成這項任務。

  • 第九行:toastStore.addToast({ action: locked ? "lock" : "unlock", hex });
      事件處理器的工作很簡單,當更新之後的 locked 狀態為 true,就用 addToast 這個方法新增一個「色票上鎖」的通知。反之,當更新之後的 locked 狀態為 false,就用 addToast 這個方法新增一個「色票解鎖」的通知。

  • 第二十行:on:change={() => handleLock({ locked, hex })}
      並在 <input type="checkbox"> 這個 HTML 元素上放入我們需要的事件處理器 handleLock。

https://i.ibb.co/wB0jKpX/26.gif
圖二、我鎖起來了!我又解鎖了!我又鎖起來了!我又又解鎖了!

  那麼這就是今天關於 Store 的實作部分了,詳細的程式碼可以參考文末附錄,或是在 Github 資源庫上也可以找到。謝謝大家。

附錄:Toast.svelte

/src/lib/Toast.svelte
<script>
  import { toastStore } from "./toastStore";
  import plus from "../assets/plus.svg";
  import minus from "../assets/minus.svg";
  import lock from "../assets/lock.svg";
  import unlock from "../assets/unlock.svg";
  export let toast;

  const icons = { plus, minus, lock, unlock };
  $: icon = icons[toast.action];
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
  <div class="icon">
    <img src={icon} alt={toast.action} />
  </div>
  <div class="hex" style="color: #{toast.hex}">{toast.hex}</div>
</div>

<style>
  .toast {
    font-size: 1.2em;
    font-weight: bold;
    width: 10em;
    padding: 1em 0;
    background: #fff;
    display: flex;
    justify-content: center;
    border-radius: 0.5em;
    gap: 1em;
    position: fixed;
    bottom: 1em;
    left: 50%;
    transform: translateX(-50%);
  }

  .toast .icon {
    width: 1.2em;
  }

  .toast .hex {
    width: 5em;
  }
</style>

上一篇
第 25 天:Svelte 的互動百寶箱:Store(二)
下一篇
第 27 天:Svelte 的過場:`transition`
系列文
了不起的 Svelte 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言